'''
KML abstract base classes 
=========================

These are the abstract base classes; they do not product KML elements.

Created on Sep 28, 2010

@author: morgan
'''

from __xml__ import createElement, createEmptyElement
from __string__ import T

__all__ = ['KMLObject', 'Angles', 'Color', 'Coordinate', 'Mode', 'Boolean']


class KMLObject(object):
    """
    This is the base class of all KML objects. It should not be used directly. 
    """    
    def __init__(self, id=""):
        if not hasattr(self, "__tagName__"):
            raise AttributeError("All subclasses of KMLObject must set "+\
                                 "self.__tagName__ prior to calling parent "+
                                 "class!")
        self.id = str(id)
    
    def toXmlNode(self,kml):
        """ 
        Returns a DOM Node object for this object.  The I{kml} rgument is a 
        kml.kml object representing the KML document.
        """
        doc = kml._doc
        myNode = createEmptyElement(doc, self.__tagName__)
        if self.id:
            myNode.setAttribute("id", self.id)
        return myNode

    def toStr(self, kml, pretty = False):
        """ 
        Returns this object as an optionally I{pretty}-formatted text string. 
        The I{kml} argument is a kml.kml object representing the KML document.
        """
        theNode = self.toXmlNode(kml)
        if pretty == True:
            return theNode.toprettyxml(indent="\t",newl="\n", encoding="UTF-8")
        return theNode.toxml("UTF-8")
    
    def parseKMLNode(self, KMLNode):
        """
        Parses the DOM Node object I{KMLNode}, filling out all of the object 
        instance's properties
        """
        if not hasattr(KMLNode, "tagName"):
            raise TypeError("KMLNode was not a KML (XML) node")
        tag = KMLNode.tagName
        if tag != self.__tagName__:
            msg=T("KMLNode was not a $tag node")
            raise TypeError(msg.substitute(tag=self.__tagName__))
        if KMLNode.getAttribute("id"):
            self.id=KMLNode.getAttribute("id")
            
class Color(object):
    """ 
    A 32-bit abgr color object.  To return an abgr string for KML, just call
    the __str__ method like this:  str(Color).
    """ 
    def __init__(self, alpha=0, blue=0, green=0, red=0, value=None):
        self.alpha = alpha
        self.blue = blue
        self.green = green
        self.red = red
        if value:
            self.value = value
        
    @property
    def value(self):
        """ 
        The value of the color. If set to an abgr string, it will be converted 
        to an integer (read/write) 
        """
        return (self.alpha << 24) | (self.blue << 16) | (self.green << 8) | (self.red)
    
    @value.setter
    def value(self, value):
        if type(value) not in [str, int]:
            raise TypeError("Color value must be an abgr string or integer.")
        if type(value) == str:
            try:
                value = int(value, 16)
            except:
                raise ValueError("Convert abgr from string value failed!")
        if type(value) == int:
            self.alpha = value >> 24
            self.blue = (value >> 16) ^ (self.alpha << 8)
            self.green = (value >> 8) ^ ((self.alpha << 16) | self.blue << 8)
            self.red = value ^ \
                ((self.alpha << 24) | (self.blue << 16) | (self.green << 8))
        
        
    def __str__(self):
        return hex(self.value)[2:].zfill(8)
    
    def __repr__(self):
        d=dict(alpha=self.alpha, blue=self.blue, green=self.green, red=self.red)
        return T("kml.Color($alpha, $blue, $green, $red)").substitute(d)
    
    @property
    def red(self):
        """ The red byte of the color. Valid values are 0-255. (read/write) """
        return self._red
    
    @red.setter
    def red(self, value):
        if type(value) != int:
            raise TypeError("Expecting value to be an integer.")
        if (value < 0) or (value > 255):
            msg=T("Expecting value ($value) to be in the range of 0-255")
            raise ValueError(msg.substitute(value=hex(value)))
        else:
            self._red = value
    
    @property
    def green(self):
        """ The green byte of the color. Valid values are 0-255. (read/write) """
        return self._green
    
    @green.setter
    def green(self, value):
        if type(value) != int:
            raise TypeError("Expecting value to be an integer.")
        if (value < 0) or (value > 255):
            raise ValueError("Expecting value to be in the range of 0-255")
        else:
            self._green = value
        
    @property
    def blue(self):
        """ The blue byte of the color. Valid values are 0-255. (read/write) """
        return self._blue
    
    @blue.setter
    def blue(self, value):
        if type(value) != int:
            raise TypeError("Expecting value to be an integer.")
        if (value < 0) or (value > 255):
            raise ValueError("Expecting value to be in the range of 0-255")
        else:
            self._blue = value

    @property
    def alpha(self):
        """ The alpha byte of the color. Valid values are 0-255. (read/write) """
        return self._alpha
    
    @alpha.setter
    def alpha(self, value):
        if type(value) != int:
            raise TypeError("Expecting value to be an integer.")
        if (value < 0) or (value > 255):
            raise ValueError("Expecting value to be in the range of 0-255")
        else:
            self._alpha = value


class Coordinate(Angles):
    """
    A simple class for creating validated longitude, latitude, altitude tuples.
    
    Properties:
    @latitude: A float or int specifying latitude
    @longitude: A float or int specifying longitude
    @altitude: A float or int specifying altitude
    @coordinates: a 3-member tuple of floats or or ints specifying coordinates 
    
    You can specify Coordinate.coordinates and have the other properties
    automagically filled out, or you can specify latitude, longitude and 
    latitude and the coordinates will be automagically filled out.  All \
    properties are checked for validity.
    """
    def __init__(self, longitude=0, latitude=0, altitude=0, coordinates=None):
        self.longitude = longitude
        self.latitude = latitude
        self.altitude = altitude
        if coordinates:
            self.coordinates = coordinates       
            
    def __str__(self):
        d=dict(lon=self.longitude, lat=self.latitude, alt=self.altitude)
        return T("$lon, $lat, $alt").substitute(d)
    
    def __repr__(self):
        d=dict(module=self.__module__, lon=self.longitude, lat=self.latitude, 
               alt=self.altitude, cls=self.__class__.__name__)
        return T("$module.$cls($lon, $lat, $alt)").substitute(d)
     
    def __getitem__(self, index):
        if isinstance(index, slice):
            return self.coordinates[index.start:index.stop:index.step]
        else:
            return self.coordinates[index]
                  
    @property
    def coordinates(self):
        return self.longitude, self.latitude, self.altitude
    
    @coordinates.setter
    def coordinates(self, value):
        if type(value) in [list, tuple]:
            self.longitude, self.latitude, self.altitude = value
        elif type(value) == Coordinate:
            self.longitude, self.latitude, self.altitude = value.coordinates
        elif hasattr(value, "value") and hasattr(value, "__getitem__") and \
            type(value.value[0]) in [list, tuple] and len(value.value[0]) == 3:
                # then it's probably a kml.coordinates, so use the first value
                # for kml.Point
                self.longitude, self.latitude, self.altitude = value.value[0]
        else:
            raise TypeError("Coordinates must be a 3-way tuple or a kml.Coordinate")

    @property 
    def altitude(self):
        return self._altitude

    @altitude.setter
    def altitude(self, value):
        if not type(value) in [int, float]:
            raise TypeError("Altitude must be a number.")
        else:
            self._altitude = float(value)
    
    @property   
    def latitude(self):
        return self._latitude

    @latitude.setter
    def latitude(self, value):
        if not type(value) in [int, float]:
            raise TypeError("Latitude must be a number.")
        elif value < -90 or value > 90:
            raise ValueError("Invalid latitude.")
        else:
            self._latitude = float(value)
        
    @property 
    def longitude(self):
        return self._longitude

    @longitude.setter
    def longitude(self, value):
        if not type(value) in [int, float]:
            raise TypeError("Longitude must be a number.")
        elif value < -180 or value > 180:
            raise ValueError("Invalid longitude.")            
        else:
            self._longitude = float(value)

class Mode(KMLObject):
    """
    Base class for all *Mode objects.  It should not be used directly.
    """
    def __init__(self, mode=""):
        self.actualMode = mode
        KMLObject.__init__(self)
        
    def __str__(self):
        return self.actualMode
    
    def __repr__(self):
        d=dict(module=self.__module__, cls=self.__class__.__name__, mode=self.actualMode)
        return T("$module.$cls('$mode')").substitute(d)
    
    @property
    def validModes(self):
        """ return a list of a modes valid values. """
        return self._validModes
    
    @property 
    def defaultMode(self):
        return self.__defaultMode__
    
    @property
    def actualMode(self):
        return self._mode
    
    @actualMode.setter
    def actualMode(self,modeStr):
        if modeStr not in self._validModes:
            msg=T("Invalid mode for $tag. Expecting one of $modes")
            raise ValueError(msg.substitute(tag=self.__tagName__,
                                            modes=self._validModes ))
        if type(modeStr) == str:
            self._mode = modeStr
        else:
            self._mode = modeStr.actualMode
    
    @property
    def value(self):
        return self.actualMode

    def toXmlNode(self,kml):
        return createElement(kml._doc,self.__tagName__,self._mode)
    
    def parseKMLNode(self, KMLNode):
        KMLObject.parseKMLNode(self, KMLNode)
        self.actualMode = str(KMLNode.childNodes[0].data)

class Boolean(KMLObject):
    """ Base class for boolean value objects """
    def __init__(self, value = False):
        self._setValue(value)
        
    def __repr__(self):
        d=dict(module=self.__module__, cls=self.__class__.__name__, 
               value=self.value)
        return T("$module.$cls($value)").substitute(d)
    
    def __str__(self):
        return str(int(self.value))

    def _getValue(self):
        return self._toExtrude
    
    def _setValue(self,val):
        if type(val) != bool:
            raise TypeError("Boolean value expected.")
        if type(val) == bool:
            self._toExtrude = val
        else:
            self._toExtrude = val.value
        
    value = property(_getValue,_setValue)

    def toXmlNode(self,kml):
        return createElement(kml._doc, self.__tagName__, int(self.value))
        
    def parseKMLNode(self, KMLNode):
        KMLObject.parseKMLNode(self, KMLNode)
        self._setValue(bool(KMLNode.childNodes[0].data))

